package com.cellulam.api.config.filters;

import com.alibaba.druid.util.PatternMatcher;
import com.alibaba.druid.util.ServletPathMatcher;
import com.cellulam.framework.core.auth.UserBasicInfo;
import com.cellulam.framework.core.auth.UserClient;
import com.cellulam.framework.core.context.AppContext;
import com.cellulam.framework.core.context.AppContextKey;
import com.cellulam.framework.core.exceptions.BusinessException;
import com.cellulam.framework.core.exceptions.ForbidAnonymousException;
import com.cellulam.framework.core.log.LogClient;
import com.cellulam.framework.core.log.RequestHttpLog;
import com.cellulam.framework.core.web.BodyRequestWrapper;
import com.cellulam.framework.core.web.ResponseWrapper;
import com.cellulam.properties.SystemProperties;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class RequestFilter implements Filter {

    @Autowired(required = false)
    private UserClient userClient;

    @Autowired(required = false)
    private LogClient logClient;

    @Autowired
    private SystemProperties systemProperties;

    private static final List<String> filterMethods = Lists.newArrayList("POST", "PUT");

    private static final String PARAM_NAME_EXCLUSIONS = "exclusions";
    private static final String PARAM_EXCLUSIONS = "/health-check,*.js,*.gif,*.jpg,*.png,*.css,*.ico";
    private static final String UNKNOWN_STRING = "unknown";
    private Set<String> excludesPattern;
    private String contextPath;
    protected PatternMatcher pathMatcher = new ServletPathMatcher();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        AppContext.initContext();
        Stopwatch sp = Stopwatch.createStarted();

        if (!(request instanceof HttpServletRequest && response instanceof HttpServletResponse)) {
            throw new ServletException("HttpFilter can't handle an non-http request");
        }

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if (httpRequest.getMethod().equals(HttpMethod.OPTIONS.name())
                || isExclusion(httpRequest.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        AppContext.setValue(AppContextKey.URL, httpRequest.getRequestURI());

        if (filterMethods.contains(httpRequest.getMethod().toUpperCase()) &&
                !StringUtils.containsIgnoreCase(httpRequest.getContentType(), "form")) {
            httpRequest = new BodyRequestWrapper(httpRequest);
        }
        httpResponse = new ResponseWrapper(httpResponse);

        String path = extractFullPath(httpRequest);

        String ip = getRemoteAddr(httpRequest);

        String result = null;

        try {
            chain.doFilter(httpRequest, httpResponse);
            byte[] res = ((ResponseWrapper) httpResponse).getContent();
            if (res != null && res.length > 0) {
                result = new String(res, com.cellulam.properties.CellulamConstans.DEFAULT_ENCODE_NAME);
                response.getOutputStream().write(res);
            }
        } catch (BusinessException e) {
            log.info("access exception method: [{}], url: [{}], IP: [{}], status: [{}], code: {}, message: {}",
                    httpRequest.getMethod(),
                    path,
                    ip,
                    httpResponse.getStatus(),
                    e.getCode(),
                    e.getMessage()
            );
        } catch (ForbidAnonymousException e) {
            log.info("access exception 禁止访问: [{}], url: [{}], IP: [{}], status: [{}], code: {}, message: {}",
                    httpRequest.getMethod(),
                    path,
                    ip,
                    httpResponse.getStatus(),
                    e.getMessage()
            );
        } catch (Exception ex) {
            log.error("access exception method: [{}], url: [{}], IP: [{}], status: [{}], ex: ",
                    httpRequest.getMethod(),
                    path,
                    ip,
                    httpResponse.getStatus(),
                    ex);
            throw ex;
        } finally {
            long cost = sp.stop().elapsed(TimeUnit.MILLISECONDS);

            String url = httpRequest.getRequestURI();
            String queryString = httpRequest.getQueryString();
            String headers = this.getHeader(httpRequest);
            String requestBody = readBytesToStr(httpRequest.getInputStream());

            if (this.logClient != null && this.systemProperties.getDurableLog()) {
                RequestHttpLog requestHttpLog = RequestHttpLog.builder()
                        .method(httpRequest.getMethod())
                        .headers(headers)
                        .queryString(queryString)
                        .request(requestBody)
                        .response(result)
                        .url(url)
                        .ip(ip)
                        .message(String.valueOf(httpResponse.getStatus()))
                        .httpStatus(httpResponse.getStatus())
                        .cost(cost)
                        .build();
                this.logClient.writeAccessLog(this.getUserId(request), requestHttpLog);
            } else {
                log.debug("access: [{}], queryString: [{}]", url, queryString);
                log.debug("request: [{}]", requestBody);
                log.debug("response: [{}]", result);
            }

            log.info("access method: [{}], url: [{}], IP: [{}], status: [{}], cost time: [{}] ms",
                    httpRequest.getMethod(),
                    path,
                    ip,
                    httpResponse.getStatus(),
                    cost);
            AppContext.clear();
        }
    }

    public boolean isExclusion(String incomingURI) {
        if (excludesPattern == null) {
            return false;
        }
        String requestURI = incomingURI;
        if (contextPath != null && requestURI.startsWith(contextPath)) {
            requestURI = requestURI.substring(contextPath.length());
            if (!requestURI.startsWith("/")) {
                requestURI = "/" + requestURI;
            }
        }

        for (String pattern : excludesPattern) {
            if (pathMatcher.matches(pattern, requestURI)) {
                return true;
            }
        }

        return false;
    }

    private String extractFullPath(HttpServletRequest req) {
        StringBuilder path = new StringBuilder(req.getScheme().trim().concat("://").concat(req.getServerName()).concat(":")
                .concat(Integer.toString(req.getServerPort())).concat(req.getRequestURI()));
        String queryStr = req.getQueryString();
        if (queryStr != null) {
            path.append("?").append(req.getQueryString());
        }
        return path.toString();
    }

    private String getHeader(HttpServletRequest req) {
        Enumeration<String> headerNames = req.getHeaderNames();
        Map<String, String> headers = Maps.newHashMap();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            headers.put(headerName, req.getHeader(headerName));
        }
        return headers.toString();
    }

    @Override
    public void init(FilterConfig config) throws ServletException {
        String exclusions = PARAM_EXCLUSIONS;
        exclusions = systemProperties.getAccessIgnore() + "," + exclusions;
        if (exclusions != null && exclusions.trim().length() != 0) {
            excludesPattern = new HashSet<>(Arrays.asList(exclusions.split("\\s*,\\s*")));
        }
        log.info("request filter init");
    }

    public String getRemoteAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN_STRING.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN_STRING.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN_STRING.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        return ip;
    }


    /**
     * 读取字节
     *
     * @param inputStream
     * @return
     */
    private static String readBytesToStr(InputStream inputStream) {
        String message = "";
        byte[] buffer = new byte[1024];
        int read;
        try {
            while ((read = inputStream.read(buffer)) > -1) {
                byte[] readBytes = new byte[read];
                System.arraycopy(buffer, 0, readBytes, 0, read);
                message = message + new String(readBytes);
            }
        } catch (IOException e) {
            log.error("", e);
        }
        return message;
    }

    private long getUserId(ServletRequest request) {
        UserBasicInfo userBasicInfo = (UserBasicInfo) request.getAttribute(UserClient.USER_ATTRIBUTE_NAME);
        if (userBasicInfo != null && userBasicInfo.getUserId() != null) {
            return userBasicInfo.getUserId();
        }
        return this.userClient == null ? 0L : this.userClient.getUserId();
    }


    @Override
    public void destroy() {

    }
}
